| @@ -0,0 +1,67 @@ | ||
| 1 | +module Agents | |
| 2 | + class GapDetectorAgent < Agent | |
| 3 | + default_schedule "every_10m" | |
| 4 | + | |
| 5 | + description <<-MD | |
| 6 | + The Gap Detector Agent will watch for holes or gaps in a stream of incoming Events and generate "no data alerts". | |
| 7 | + | |
| 8 | + The `value_path` value is a [JSONPath](http://goessner.net/articles/JsonPath/) to a value of interest. If either | |
| 9 | + this value is empty, or no Events are received, during `window_duration_in_days`, an Event will be created with | |
| 10 | + a payload of `message`. | |
| 11 | + MD | |
| 12 | + | |
| 13 | + event_description <<-MD | |
| 14 | + Events look like: | |
| 15 | + | |
| 16 | +          { | |
| 17 | + "message": "No data has been received!", | |
| 18 | + "gap_started_at": "1234567890" | |
| 19 | + } | |
| 20 | + MD | |
| 21 | + | |
| 22 | + def validate_options | |
| 23 | + unless options['message'].present? | |
| 24 | + errors.add(:base, "message is required") | |
| 25 | + end | |
| 26 | + | |
| 27 | + unless options['window_duration_in_days'].present? && options['window_duration_in_days'].to_f > 0 | |
| 28 | + errors.add(:base, "window_duration_in_days must be provided as an integer or floating point number") | |
| 29 | + end | |
| 30 | + end | |
| 31 | + | |
| 32 | + def default_options | |
| 33 | +      { | |
| 34 | + 'window_duration_in_days' => "2", | |
| 35 | + 'message' => "No data has been received!" | |
| 36 | + } | |
| 37 | + end | |
| 38 | + | |
| 39 | + def working? | |
| 40 | + true | |
| 41 | + end | |
| 42 | + | |
| 43 | + def receive(incoming_events) | |
| 44 | + incoming_events.sort_by(&:created_at).each do |event| | |
| 45 | + memory['newest_event_created_at'] ||= 0 | |
| 46 | + | |
| 47 | + if !interpolated['value_path'].present? || Utils.value_at(event.payload, interpolated['value_path']).present? | |
| 48 | + if event.created_at.to_i > memory['newest_event_created_at'] | |
| 49 | + memory['newest_event_created_at'] = event.created_at.to_i | |
| 50 | +            memory.delete('alerted_at') | |
| 51 | + end | |
| 52 | + end | |
| 53 | + end | |
| 54 | + end | |
| 55 | + | |
| 56 | + def check | |
| 57 | + window = interpolated['window_duration_in_days'].to_f.days.ago | |
| 58 | + if memory['newest_event_created_at'].present? && Time.at(memory['newest_event_created_at']) < window | |
| 59 | + unless memory['alerted_at'] | |
| 60 | + memory['alerted_at'] = Time.now.to_i | |
| 61 | +          create_event payload: { message: interpolated['message'], | |
| 62 | + gap_started_at: memory['newest_event_created_at'] } | |
| 63 | + end | |
| 64 | + end | |
| 65 | + end | |
| 66 | + end | |
| 67 | +end | 
| @@ -7,7 +7,7 @@ module Agents | ||
| 7 | 7 | description <<-MD | 
| 8 | 8 |        The Peak Detector Agent will watch for peaks in an event stream.  When a peak is detected, the resulting Event will have a payload message of `message`.  You can include extractions in the message, for example: `I saw a bar of: {{foo.bar}}`, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details. | 
| 9 | 9 |  | 
| 10 | - The `value_path` value is a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value of interest. `group_by_path` is a hash path that will be used to group values, if present. | |
| 10 | + The `value_path` value is a [JSONPath](http://goessner.net/articles/JsonPath/) to the value of interest. `group_by_path` is a JSONPath that will be used to group values, if present. | |
| 11 | 11 |  | 
| 12 | 12 | Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. | 
| 13 | 13 |  | 
| @@ -0,0 +1,112 @@ | ||
| 1 | +require 'spec_helper' | |
| 2 | + | |
| 3 | +describe Agents::GapDetectorAgent do | |
| 4 | +  let(:valid_params) { | |
| 5 | +    { | |
| 6 | + 'name' => "my gap detector agent", | |
| 7 | +      'options' => { | |
| 8 | + 'window_duration_in_days' => "2", | |
| 9 | + 'message' => "A gap was found!" | |
| 10 | + } | |
| 11 | + } | |
| 12 | + } | |
| 13 | + | |
| 14 | +  let(:agent) { | |
| 15 | + _agent = Agents::GapDetectorAgent.new(valid_params) | |
| 16 | + _agent.user = users(:bob) | |
| 17 | + _agent.save! | |
| 18 | + _agent | |
| 19 | + } | |
| 20 | + | |
| 21 | + describe 'validation' do | |
| 22 | + before do | |
| 23 | + expect(agent).to be_valid | |
| 24 | + end | |
| 25 | + | |
| 26 | + it 'should validate presence of message' do | |
| 27 | + agent.options['message'] = nil | |
| 28 | + expect(agent).not_to be_valid | |
| 29 | + end | |
| 30 | + | |
| 31 | + it 'should validate presence of window_duration_in_days' do | |
| 32 | + agent.options['window_duration_in_days'] = "" | |
| 33 | + expect(agent).not_to be_valid | |
| 34 | + | |
| 35 | + agent.options['window_duration_in_days'] = "wrong" | |
| 36 | + expect(agent).not_to be_valid | |
| 37 | + | |
| 38 | + agent.options['window_duration_in_days'] = "1" | |
| 39 | + expect(agent).to be_valid | |
| 40 | + | |
| 41 | + agent.options['window_duration_in_days'] = "0.5" | |
| 42 | + expect(agent).to be_valid | |
| 43 | + end | |
| 44 | + end | |
| 45 | + | |
| 46 | + describe '#receive' do | |
| 47 | + it 'records the event if it has a created_at newer than the last seen' do | |
| 48 | + agent.receive([events(:bob_website_agent_event)]) | |
| 49 | + expect(agent.memory['newest_event_created_at']).to eq events(:bob_website_agent_event).created_at.to_i | |
| 50 | + | |
| 51 | + events(:bob_website_agent_event).created_at = 2.days.ago | |
| 52 | + | |
| 53 | +      expect { | |
| 54 | + agent.receive([events(:bob_website_agent_event)]) | |
| 55 | +      }.to_not change { agent.memory['newest_event_created_at'] } | |
| 56 | + | |
| 57 | + events(:bob_website_agent_event).created_at = 2.days.from_now | |
| 58 | + | |
| 59 | +      expect { | |
| 60 | + agent.receive([events(:bob_website_agent_event)]) | |
| 61 | +      }.to change { agent.memory['newest_event_created_at'] }.to(events(:bob_website_agent_event).created_at.to_i) | |
| 62 | + end | |
| 63 | + | |
| 64 | + it 'ignores the event if value_path is present and the value at the path is blank' do | |
| 65 | + agent.options['value_path'] = 'title' | |
| 66 | + agent.receive([events(:bob_website_agent_event)]) | |
| 67 | + expect(agent.memory['newest_event_created_at']).to eq events(:bob_website_agent_event).created_at.to_i | |
| 68 | + | |
| 69 | + events(:bob_website_agent_event).created_at = 2.days.from_now | |
| 70 | + events(:bob_website_agent_event).payload['title'] = '' | |
| 71 | + | |
| 72 | +      expect { | |
| 73 | + agent.receive([events(:bob_website_agent_event)]) | |
| 74 | +      }.to_not change { agent.memory['newest_event_created_at'] } | |
| 75 | + | |
| 76 | + events(:bob_website_agent_event).payload['title'] = 'present!' | |
| 77 | + | |
| 78 | +      expect { | |
| 79 | + agent.receive([events(:bob_website_agent_event)]) | |
| 80 | +      }.to change { agent.memory['newest_event_created_at'] }.to(events(:bob_website_agent_event).created_at.to_i) | |
| 81 | + end | |
| 82 | + | |
| 83 | + it 'clears any previous alert' do | |
| 84 | + agent.memory['alerted_at'] = 2.days.ago.to_i | |
| 85 | + agent.receive([events(:bob_website_agent_event)]) | |
| 86 | +      expect(agent.memory).to_not have_key('alerted_at') | |
| 87 | + end | |
| 88 | + end | |
| 89 | + | |
| 90 | + describe '#check' do | |
| 91 | + it 'alerts once if no data has been received during window_duration_in_days' do | |
| 92 | + agent.memory['newest_event_created_at'] = 1.days.ago.to_i | |
| 93 | + | |
| 94 | +      expect { | |
| 95 | + agent.check | |
| 96 | +      }.to_not change { agent.events.count } | |
| 97 | + | |
| 98 | + agent.memory['newest_event_created_at'] = 3.days.ago.to_i | |
| 99 | + | |
| 100 | +      expect { | |
| 101 | + agent.check | |
| 102 | +      }.to change { agent.events.count }.by(1) | |
| 103 | + | |
| 104 | +      expect(agent.events.last.payload).to eq ({ 'message' => 'A gap was found!', | |
| 105 | + 'gap_started_at' => agent.memory['newest_event_created_at'] }) | |
| 106 | + | |
| 107 | +      expect { | |
| 108 | + agent.check | |
| 109 | +      }.not_to change { agent.events.count } | |
| 110 | + end | |
| 111 | + end | |
| 112 | +end |